/*
 * Schedular.java
 * Remains a java only processor acting as a hidden layer between a calendar application and local/web services
 */


import java.io.*;
import java.util.*;
import java.net.*;
import java.text.*;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.httpclient.params.HttpMethodParams;


class Schedular {

	Hashtable settings; //Settings read from settings file
	static String conf_file = "calendar_server.conf"; //Global conf file, can be overidden

	/**
	 * Main Method for stand alone schedular to this can be parsed several variables.
	 * These can be viewed by running the Schedular with --help as the first argument.
	 */
	public static void main(String[] args) {
		try {
			if (args[0].equals("--help")) {
				System.out.println("Server Schedular (v0.1)");
				System.out.println("");
				System.out.println("Usage:");
				System.out.println("");
				System.out.println(" -c    Path to Configuration File");
				System.out.println("");
				System.exit(0);
			} else {
				/* 
				 * Handles parsed variables from command line.
				 */
				while (args.length > 0) {
					if (args[0].equals("-c")) {
						conf_file = args[1];
					}
					if (args.length > 2) {
						String[] foo = new String[args.length-2];
						for (int i=2;i<args.length;i++) {
							foo[i-2] = args[i];
						}
						args = foo;
					} else {
						args = new String[0];
					}
				}
			}
		} catch (Exception e) {
		}
		/*
		 * Launch a Schedular
		 */
		Schedular sch = new Schedular(conf_file);
	}
	/**
	 * Basic Schedular
	 * Gets settings and then calls get_events which actually processes the event as well
	 *
	 * @param	conf_file	A String giving the absolute or relative to classpath location of the conf_file.
	 */
	Schedular(String conf_file) {
		settings = getConfSettings(conf_file);
		get_events();
	}

	/**
	 * Retrieves the configuration settings from the specified file
	 *
	 * @param       conf_file       A String giving the absolute or relative to classpath location of the conf_file.
	 * @return		Key/Value pairs read from Configuration File.
	 */
	private Hashtable getConfSettings(String conf_file) {
		settings = new Hashtable();
		try {
			BufferedReader in = new BufferedReader(new FileReader(conf_file));
			String line;
			while ((line = in.readLine()) != null) {
				line = line.trim();
				if (!line.equals("")) {
					if (!line.substring(0,1).equals("#")) {
						String[] sline = line.split("=");
						try { 
							String slineout = "";
							try {
								for(int i=1;i<sline.length;i++) {
									slineout += sline[i];
								}
							} catch (Exception e) {}
							sline[1] = slineout;
							settings.put(sline[0].trim(),sline[1].trim());
						} catch (ArrayIndexOutOfBoundsException a) {
							settings.put(sline[0].trim(),"");
						}
					}
				}
			}
			in.close();
		} catch (IOException e) {
			System.out.println("Failed to parse log file.");
		}
		return settings;
	} 
	
	/**
	 * get_events is the main routine which should only be called once you have read in the configuraton file.
	 * It is pretty much just an extension of the main schedular object
	 */
	public void get_events() {
		String calendar_url = (String)settings.get("calendar_server");
		String calendar = (String)settings.get("calendar_path");
		Hashtable locks = new Hashtable();
		
		/*
		 *  Get a list of locked events
		 */
		try {
			BufferedReader in = new BufferedReader(new FileReader(".locks"));
			String str;
			while ((str = in.readLine()) != null) {
				locks.put(str,1);
			}
		} catch (IOException e) {
			//print to log file?
		}

		/*
		 * Get a list of the events and find those which can or need to be done
		 * It is in this section we need to find repeated events and handle them, each one will need a new URI.
		 * What to do with the existing weekly event is the problem, do you need to just update it to start from the 
		 * following week?
		 */
		try {
			URL ics_file = new URL(calendar_url + calendar);
			Hashtable event = new Hashtable();
			
			BufferedReader in = new BufferedReader(new InputStreamReader(ics_file.openStream()));
			String inputLine;
			boolean inevent = false;
			String key = "";
			String rest = "";
			/*
			 * Iterate over the events and process them one by one
			 */
			while ((inputLine = in.readLine()) != null) {
				if (inputLine.equals("BEGIN:VEVENT")) {
					inevent = true;
				}
				while (inevent == true) {
					inputLine = in.readLine();
					if (inputLine.equals("END:VEVENT")) {
						inevent = false;
						String uid = (String)event.get("UID");
						int lock = 0;
						try {
							lock = (Integer)locks.get(uid);
						} catch (Exception e) {}
						if (lock == 0) {
							process_event(event);
						} else {
							//System.out.println(uid + " LOCKED");
						}
					}
					if (inputLine.substring(0,1).equals(" ")) {
						rest = rest + inputLine;
						event.put(key,rest);
					} else {
						String[] in_bits = inputLine.split(":",2);
						key = in_bits[0];
						rest = in_bits[1];
						try { 
							// Needed when DTSTART has crap in it. We are not supporting time zones in this version.
							String[] sub_key = key.split(";",2);
							key = sub_key[0];
						} catch (Exception e) {
							e.printStackTrace();
						}

						event.put(key,rest);
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
			//write to a log file?
		}
	}
	
	/**
	 * Locks a uid so that another scheduling process doen't try to process the same on.
	 * You are not meant to have 2 schedulars both start running at the same time, you can have one running when you start 
	 * another as this routine is run early on before any heavy loading processes/web services are called.
	 *
	 * @param	uid	The uid to lock
	 */
	private void lock(String uid) {
		try {
			BufferedWriter out = new BufferedWriter(new FileWriter(".locks",true));
			out.write(uid);
			out.newLine();
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
			//print to log file?
		}
	}
	
	/**
	 * NOT USED
	 * The log writing routine, a simple one that just keeps adding to the same log file.
	 * All Log entries will be either displayed or screen or caught by a cron daemon since this is the recommended way of 
	 * running the schedular.
	 *
	 * @param	item	Something to write to the log file
	 */
	private void log_write(String item) {
		try {
			String local_logs = (String)settings.get("local_log");
			BufferedWriter out = new BufferedWriter(new FileWriter(local_logs+"/Schedular.log",true));
			out.write(item);
			out.newLine();
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
			//print to log file?
		}
	}

	/** 
	 * Reads in from the single event return which DroidWrapper provides.
	 * This is then processed ready for writing back to the master iCal server/file
	 *
	 * @param	file_name	The file to read
	 * @return		The event from the file, minus BEGIN and END tags.
	 */
	public Hashtable get_event(String file_name) throws Exception {
		Hashtable event = new Hashtable();
		BufferedReader in = new BufferedReader(new FileReader(file_name));
		String inputLine;
		boolean inevent = false;
		String key = "";
		String rest = "";
		while ((inputLine = in.readLine()) != null) {
			if (inputLine.equals("BEGIN:VEVENT")) {
				inevent = true;
			}
			while (inevent == true) {
				inputLine = in.readLine();
				if (inputLine.equals("END:VEVENT")) {
					inevent = false;
				}
				if (inputLine.substring(0,1).equals(" ")) {
					rest = rest + inputLine;
					event.put(key,rest);
				} else {
					String[] in_bits = inputLine.split(":",2);
					key = in_bits[0];
					rest = in_bits[1];
					try { 
						String[] sub_key = key.split(";",2);
						key = sub_key[0];
					} catch (Exception e) {
						e.printStackTrace();
					}

					event.put(key,rest);
				}
			}
		}
		return event;
	}

	/**
	 * Once we have found an event to do this routine processes it and calls the local/web service which relates
	 * to this event.
	 *
	 * @param 	event	name/value pairs from the ics file, between BEGIN:VEVENT and END:VEVENT
	 */
	public void process_event(Hashtable event) {
		
		String start = (String)event.get("DTSTART");
		String summary = (String)event.get("SUMMARY");
		String description = (String)event.get("DESCRIPTION");
		String uid = (String)event.get("UID");
		String local_logs = (String)settings.get("local_log");
		
		if (summary.equals("droid") && description.equals("do_classification")) {

			
			start = start.replace("T","");
			start = start.replace("t","");
			start = start.replace("Z","");
			start = start.replace("z","");

			SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddhhmmss");
			long eventdate = 0;
			try {
				Date mydate = sdf.parse(start);
				eventdate = mydate.getTime();
			} catch (Exception e) {}
			long current = new Date().getTime();
			
			if (eventdate < current) {
				try {
					lock(uid);
					event.put("SUMMARY","Schedular");
					event.put("DESCRIPTION","Event Running");
					iCal_to_server(event);
					DroidWrapper dw = new DroidWrapper(uid,0);
					boolean exists = false;
					String droid_ical = local_logs+uid+".ics";
					while (exists == false) {
						try {
							Thread.sleep(1000);
							exists = (new File(droid_ical)).exists();
						} catch (Exception e) {
							e.printStackTrace();
							System.exit(0);
						}
					}
					Hashtable event_return = get_event(droid_ical);
					event = combine(event,event_return);
					iCal_to_server(event);
				} catch (Exception e) {
					e.printStackTrace();
					System.exit(0);
				}
			}
		}
	}

	/**
	 * Combines the returned event ics with the one the master server/path has
	 * We need to keep the original start time but override the rest basically
	 *
	 * @param	master	The original event hashtable which was handed to the process_event method
	 * @param	slave	The ics file part returned by our web service wrapper
	 * @return 		The combined event
	 */
	private Hashtable combine(Hashtable master,Hashtable slave) {
		master.put("DTEND",slave.get("DTEND"));
		master.put("SUMMARY",slave.get("SUMMARY"));
		master.put("DESCRIPTION",slave.get("DESCRIPTION"));
		master.put("STATUS",slave.get("STATUS"));
		return master;
	}

	/**
	 * This is the method to ensure that each line in the ics file is no longer than 75 octects.
	 *
	 * @param	in	The String to check is the correct length
	 * @return		The proccessed and conforming string
	 */
	private String reduce(String in) {
		char[] letters = in.toCharArray();
		if (letters.length < 75) {
			in += "\n";
			return in;
		} else {
			String ret = "\n";
			String sp = " ";
			char[] newLine = ret.toCharArray();
			char[] space = sp.toCharArray();
			int count =0;
			int first =1;
			String whole = "";
			while (count < letters.length) {
				char[] out_array = new char[75];
				if (first == 1) {
					out_array[0] = letters[0];
					count++;
					first = 0;
				} else {
					out_array[0] = space[0];
				}
				int i=0;
				for (i=1;i<74;i++) {
					try {
						out_array[i] = letters[count];
					} catch (Exception e) {
						break;
					}
					count++;
				}
				out_array[i] = newLine[0];
				whole += String.valueOf(out_array);
			}
			return whole;
		}
	}

	/**
	 * Writes the processed event back to the server
	 *
	 * @param	event	The singular event to be written
	 */
	public void iCal_to_server(Hashtable event) {
		String cal_out = "BEGIN:VEVENT\n";
		String success = "true";
		String calendar_url = (String)settings.get("calendar_write_server");
		String calendar = (String)settings.get("calendar_path");
		
		HttpClient client = new HttpClient();
		
		PutMethod method  = new PutMethod(calendar_url + calendar);
		method.setRequestHeader("Content-Type", "text/icalendar");
		method.setRequestHeader("If-Match", "*");

		Hashtable pre_done = new Hashtable();
		pre_done.put("DTSTART",1);
		pre_done.put("DTEND",1);
		pre_done.put("UID",1);
		pre_done.put("SUMMARY",1);
		pre_done.put("DESCRIPTION",1);

		cal_out += "DTSTART:" + (String)event.get("DTSTART") + "\n";
		cal_out += "DTEND:" + (String)event.get("DTEND") + "\n";
		cal_out += "UID:" + (String)event.get("UID") + "\n";
		cal_out += reduce("SUMMARY:" + (String)event.get("SUMMARY"));
		cal_out += reduce("DESCRIPTION:" + (String)event.get("DESCRIPTION"));


		for (Enumeration keys=event.keys(); keys.hasMoreElements();) {
			String key = (String)keys.nextElement();
			String value = (String)event.get(key);
			try {
				if ((Integer)pre_done.get(key) != 1) {
					cal_out += reduce(key + ":" + value);
				}
			} catch (Exception e) {}
		}
		cal_out += "END:VEVENT\n";
		method.setRequestBody(cal_out);

		// Provide custom retry handler is necessary
		method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
				new DefaultHttpMethodRetryHandler(3, false));

		try {
			// Execute the method.
			int statusCode = client.executeMethod(method);

			if (statusCode < 200 || statusCode > 299) {
				success = "false";
				System.err.println("Method failed: " + method.getStatusLine());
			}

			// Read the response body.
			byte[] responseBody = method.getResponseBody();

			// Deal with the response.
			// Use caution: ensure correct character encoding and is not binary data
			//System.out.println(new String(responseBody));

		} catch (HttpException e) {
			System.err.println("Fatal protocol violation: " + e.getMessage());
			success = "false";
			e.printStackTrace();
		} catch (IOException e) {
			System.err.println("Fatal transport error: " + e.getMessage());
			success = "false";
			e.printStackTrace();
		} finally {
			// Release the connection.
			method.releaseConnection();
		} 
	}
}
